/**
 * Copyright 2018-2019 jianggujin (www.jianggujin.com).
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.jianggujin.modulelink.util;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.jianggujin.modulelink.util.JLogFactory.JLog;
import com.jianggujin.modulelink.util.vfs.JVFS;

/**
 * 解析工具，用于定位查找类
 * 
 * @author jianggujin
 *
 */
public class JResolverUtils {
    private static final JLog logger = JLogFactory.getLog(JResolverUtils.class);

    private Set<Class<?>> matches = new HashSet<Class<?>>();
    /**
     * 需要排除的配置文件
     */
    private Set<String> exclusions = new HashSet<String>();

    private ClassLoader classloader;

    public Set<String> getExclusions() {
        return exclusions;
    }

    public void setExclusions(Set<String> exclusions) {
        JAssert.checkNotNull(exclusions, "exclusions must not be null");
        this.exclusions = exclusions;
    }

    public JResolverUtils withExclusions(Set<String> exclusions) {
        this.setExclusions(exclusions);
        return this;
    }

    public JResolverUtils addExclusions(Collection<? extends String> exclusions) {
        if (exclusions != null) {
            this.exclusions.addAll(exclusions);
        }
        return this;
    }

    public JResolverUtils addExclusion(String exclusion) {
        JAssert.checkNotNull(exclusion, "exclusion must not be null");
        this.exclusions.add(exclusion);
        return this;
    }

    public JResolverUtils removeExclusion(String exclusion) {
        this.exclusions.remove(exclusion);
        return this;
    }

    public Set<Class<?>> getClasses() {
        return matches;
    }

    public ClassLoader getClassLoader() {
        return classloader == null ? Thread.currentThread().getContextClassLoader() : classloader;
    }

    public void setClassLoader(ClassLoader classloader) {
        this.classloader = classloader;
    }

    public JResolverUtils findImplementations(Class<?> parent, String... packageNames) {
        if (packageNames == null) {
            return this;
        }

        find(new JIsA(parent), packageNames);
        return this;
    }

    public JResolverUtils findAnnotated(Class<? extends Annotation> annotation, String... packageNames) {
        if (packageNames == null) {
            return this;
        }

        find(new JAnnotatedWith(annotation), packageNames);
        return this;
    }

    public JResolverUtils find(JTest test, String... packageNames) {
        for (String packageName : packageNames) {
            String path = getPackagePath(packageName);

            try {
                List<String> children = JVFS.getInstance().list(path);
                for (String child : children) {
                    if (child.endsWith(".class")) {
                        addIfMatching(test, child);
                    }
                }
            } catch (IOException ioe) {
                logger.error("find error", ioe);
            }
        }

        return this;
    }

    protected String getPackagePath(String packageName) {
        return packageName == null ? null : packageName.replace('.', '/');
    }

    protected void addIfMatching(JTest test, String fqn) {
        try {
            String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
            if (exclusions.contains(externalName)) {
                return;
            }
            ClassLoader loader = getClassLoader();
            Class<?> type = loader.loadClass(externalName);
            if (test.matches(type)) {
                if (logger.isDebugEnabled()) {
                    logger.debug("find match class:" + type);
                }
                matches.add((Class<?>) type);
            }
        } catch (Throwable t) {
            logger.error("addIfMatching error", t);
        }
    }

    /**
     * 一个简单的测试接口用于判断类是否应该包含在返回结果中
     */
    public static interface JTest {
        boolean matches(Class<?> type);
    }

    public static class JIsA implements JTest {
        private Class<?> parent;

        public JIsA(Class<?> parentType) {
            this.parent = parentType;
        }

        @Override
        public boolean matches(Class<?> type) {
            return type != null && parent.isAssignableFrom(type);
        }

        @Override
        public String toString() {
            return "is assignable to " + parent.getSimpleName();
        }
    }

    public static class JAnnotatedWith implements JTest {
        private Class<? extends Annotation> annotation;

        public JAnnotatedWith(Class<? extends Annotation> annotation) {
            this.annotation = annotation;
        }

        @Override
        public boolean matches(Class<?> type) {
            return type != null && type.isAnnotationPresent(annotation);
        }

        @Override
        public String toString() {
            return "annotated with @" + annotation.getSimpleName();
        }
    }

    public static class JCombinedTest implements JTest {
        private final JTest[] tests;

        public JCombinedTest(JTest... tests) {
            this.tests = tests;
        }

        @Override
        public boolean matches(Class<?> type) {
            for (JTest test : tests) {
                if (test.matches(type)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public String toString() {
            return Arrays.toString(tests);
        }
    }
}
